<?php
/**
 * Apricot ExceptionHandler file
 *
 * <pre>
 *     _               _           _
 *    / \   _ __  _ __(_) ___ ___ | |_
 *   / _ \ | '_ \| '__| |/ __/ _ \| __|
 *  / ___ \| |_) | |  | | (_| (_) | |_
 * /_/   \_\ .__/|_|  |_|\___\___/ \__|
 *         |_|
 * </pre>
 *
 * @package Apricot
 * @version $Id: ExceptionHandler.php 1734 2010-03-16 02:49:55Z jansen $
 */

/** Apricot_Controller */
require_once 'Apricot/Controller.php';

/**
 * Apricot_ExceptionHandler
 *
 * @package Apricot
 * @author Jansen Price <jansen.price@gmail.com>
 * @license http://www.opensource.org/licenses/mit-license.php MIT
 * @version ApricotVersion: 1.1
 */
class Apricot_ExceptionHandler
{
    /**
     * Storage of kernel object
     *
     * @var object
     */
    protected static $_kernel = null;

    /**
     * Init the error handlers
     *
     * @param Apricot_Kernel $kernel Kernel object
     * @return void
     */
    public static function initHandlers(Apricot_Kernel $kernel)
    {
        self::setKernel($kernel);

        set_exception_handler(array('Apricot_ExceptionHandler', 'handle'));
        set_error_handler(array('Apricot_ExceptionHandler', 'handleError'));
        register_shutdown_function(
            array('Apricot_ExceptionHandler', 'handleShutdown')
        );
    }

    /**
     * Set the kernel object
     *
     * @param mixed $kernel Apricot kernel object
     * @return void
     */
    public static function setKernel($kernel)
    {
        self::$_kernel = $kernel;
    }

    /**
     * Handle an error
     *
     * @return void
     */
    public static function handleError()
    {
        list($errno, $message, $file, $line) = func_get_args();

        $message = self::_error_code($errno)
            . ": " . $message . " in " . $file . ":" . $line;

        if (self::$_kernel->getConfig('display_errors')) {
            echo $message . "<br />";
        }
        self::$_kernel->log($message, 'errors');
    }

    /**
     * Handle a shutdown
     *
     * @return void
     */
    public static function handleShutdown()
    {
        $error = error_get_last();

        if (!empty($error)) {
            // This way fatal errors will get logged as well.
            self::handleError(
                $error['type'], $error['message'],
                $error['file'], $error['line']
            );
        }
        exit();
    }

    /**
     * Handle thrown exceptions
     *
     * @param Exception $e The Exception object
     * @return void
     */
    public static function handle(Exception $e)
    {
        $ak = self::$_kernel;

        $exception_type = get_class($e);

        $http_code = self::_get_http_code($e);
        $header    = self::_header_by_code($http_code);

        // detect terminal
        if (!$ak->isTerminal()) {
            header(
                "X-Apricot-Exception: "
                . $e->getMessage() . " - " . self::get_informative_message($e)
            );
            header($header);
        } else {
            $error_number = self::log_exception($e);
            $terminal     = $ak->getTerminal();
            echo "\n";
            $terminal->pretty_message(
                $e->getMessage() . " (Logged as $error_number)", 7, 1
            );
            echo "\n";
            //exit(1);
            return;
        }

        if ($ak->getRequest()->isXhr()) {
            $messages = array(
                'result'        => 'error',
                'error_message' => $e->getMessage(),
                'error_details' => self::get_informative_message($e),
            );
            echo json_encode($messages);
            //exit(0);
            return;
        }

        // Set up a request with the error information
        //  so it can be dispatched to the error_controller
        $error_request = new Apricot_RequestHttp(
            array(
                'controller' => 'error',
                'action'     => 'index',
                'httpcode'   => $http_code,
                'exception'  => $e,
            ),
            $ak->getConfig()
        );

        try {
            $controller_class_name = $ak->loadController('error');
            $ak->dispatch($error_request);
        }
        catch (Exception $err) {
            if (!($err->getCode()
                & Apricot_KernelException::ERROR_LOAD_CONTROLLER)
            ) {
                // We couldn't handle the exception in layout.
                echo "<h2>Error: " . $e->getMessage() . "</h2>";
                echo self::display_exception($e);
            } else {
                try {
                    // The app's controller_error didn't exist, use the default
                    $controller = new ExceptionHandler_ControllerError(
                        $ak, $error_request
                    );
                    //echo $controller->getVbuf();
                    if ($e->getMessage() != $err->getMessage()) {
                        echo "<h3>" . $e->getMessage() . "</h3>"
                            . self::display_exception($e)
                            . "<h3>Additionally, " . $err->getMessage()
                            . "</h3>"
                            . self::display_exception($err);
                    }
                } catch (Exception $failsafe) {
                    // Something horrible happened
                    echo "<h2>Failsafe Error: "
                        . $failsafe->getMessage() . "</h2>";
                    echo self::display_exception($failsafe);
                }
            }
        }
    }

    /**
     * Get the message from the exception that includes the file and line number
     *
     * @param mixed $e Exception object
     * @return void
     */
    public static function get_informative_message($e)
    {
        return "Error code #" . $e->getCode()
            . " in file " . $e->getFile()
            . " on line " . $e->getLine() . ".";
    }

    /**
     * display_exception
     *
     * @param mixed $e Exception object
     * @return string
     */
    public static function display_exception($e)
    {
        $out  = "";
        $out .= "<p>"
            . get_class($e) . ": "
            . self::get_informative_message($e) . "</p>";
        $out .= "<p>Trace:</p>";

        $trace = $e->getTrace();
        $out  .= "<table><tr><th>#</th><th>function</th>"
            . "<th>location</th><th>args</th></tr>";
        foreach ($trace as $i=>$tl) {
            $file  = isset($tl['file']) ? $tl['file'] : '';
            $class = isset($tl['class']) ? $tl['class'] : 'main';
            $line  = isset($tl['line']) ? $tl['line'] : '0';
            $out  .= "<tr>"
                ."<td>" . $i . "</td>"
                ."<td>" . $class . "::" .  $tl['function'] . "()</td>"
                ."<td>" . $file  . ":" . $line . "</td>"
                ."<td>" . self::display_trace_args($tl['args']). "</td>"
                ."</tr>";
        }
        $out .= "</table>";
        return $out;
    }

    /**
     * display_args
     *
     * @param mixed $args Arguments to display
     * @param string $glue The glue used to implode() the args if array
     * @return string
     */
    public static function display_trace_args($args, $glue="<br />")
    {
        $out = '';
        if (is_array($args)) {
            foreach ($args as $arg) {
                if (is_object($arg)) {
                    $out .= get_class($arg) . $glue;
                } else {
                    $out .= $arg . $glue;
                }
            }
        } else {
            $out .= $args;
        }

        return $out;
    }

    /**
     * trace_text_table
     *
     * @param mixed $trace The trace array
     * @return string
     */
    public static function trace_text_table($trace)
    {
        $out = '';

        $out .= "#\tFunction\tLocation\tArgs\n";

        foreach ($trace as $i=>$tl) {
            $class = isset($tl['class']) ? $tl['class'] : 'main';
            $file  = isset($tl['file']) ? $tl['file'] : '';
            $line  = isset($tl['line']) ? $tl['line'] : '0';
            $out  .= $i . "\t"
                . $class . "::" . $tl['function'] . "()\t"
                . $file . ":" . $line . "\t"
                . self::display_trace_args($tl['args'], ",")
                . "\n";
        }
        return $out;
    }

    /**
     * Get the correct http code from the exception
     *
     * @param object $e The Exception object
     * @return int
     */
    private static function _get_http_code($e)
    {
        if ($e->getCode() & Apricot_KernelException::ERROR_LOAD) {
            $http_code = 404;
        } else {
            $http_code = 500;
        }
        return $http_code;
    }

    /**
     * Get the correct http header status code string
     *
     * @param int $code The http status code
     * @return string
     */
    private static function _header_by_code($code)
    {
        $headers = array (
            200 => 'HTTP/1.0 200 OK',
            303 => 'HTTP/1.0 303 See Other',
            304 => 'HTTP/1.0 304 Not Modified',
            401 => 'HTTP/1.0 401 Unauthorized',
            404 => 'HTTP/1.0 404 Not Found',
            405 => 'HTTP/1.0 405 Method not allowed',
            500 => 'HTTP/1.0 500 Server Error',
            503 => 'HTTP/1.0 503 Service Unavailable',
        );

        if (!isset($headers[$code])) {
            $code = 500;
        }

        return $headers[$code];
    }

    /**
     * Convert an error code into the PHP error constant name
     *
     * @param int $code The PHP error code
     * @return string
     */
    private static function _error_code($code)
    {
        $error_levels = array(
            1     => 'E_ERROR',
            2     => 'E_WARNING',
            4     => 'E_PARSE',
            8     => 'E_NOTICE',
            16    => 'E_CORE_ERROR',
            32    => 'E_CORE_WARNING',
            64    => 'E_COMPILE_ERROR',
            128   => 'E_COMPILE_WARNING',
            256   => 'E_USER_ERROR',
            512   => 'E_USER_WARNING',
            1024  => 'E_USER_NOTICE',
            2048  => 'E_STRICT',
            4096  => 'E_RECOVERABLE_ERROR',
            8192  => 'E_DEPRECATED',
            16384 => 'E_USER_DEPRECATED',
        );
        return $error_levels[$code];
    }

    /**
     * Log an exception
     *
     * @param mixed $e The Exception object
     * @return void
     */
    public static function log_exception($e)
    {
        $ak = self::$_kernel;

        if ($ak->isTerminal()) {
            $log_file = "exceptions-terminal";
        } else {
            $log_file = "exceptions";
        }

        $log_error_number = self::_generate_random_string();

        $request_uri     = isset($_SERVER['REQUEST_URI'])
            ? $_SERVER['REQUEST_URI'] : '';
        $http_user_agent = isset($_SERVER['HTTP_USER_AGENT'])
            ? $_SERVER['HTTP_USER_AGENT'] : '';

        $log_message = "---------------------------------------\n"
            . "ERROR NUMBER: $log_error_number\n"
            . "DATE: " . date('Y-m-d H:i:s') . "\n"
            . "EXCEPTION TYPE: " . get_class($e) . "\n"
            . "MESSAGE: " . $e->getMessage() . "\n"
            . "CODE: " . $e->getCode() . "\n"
            . "HTTPCODE: " . self::_get_http_code($e) . "\n"
            . "REQUEST URI: " . $request_uri . "\n"
            . "REFERER: " . $ak->getRequest()->getHeaderParam('HTTP_REFERER')
            . "\n"
            . "HTTP USER AGENT: " . $http_user_agent . "\n"
            . "TRACE:\n" . self::trace_text_table($e->getTrace()) . "\n"
            ;

        $ak->log($log_message, $log_file);
        return $log_error_number;
    }

    /**
     * Generate a random string that can be used as a filename
     *
     * @param int $length The length of the filename
     * @return string
     */
    private static function _generate_random_string($length=4)
    {
        $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

        $string = '';
        for ($i = 0; $i < $length; ++$i) {
            $string .= $characters[mt_rand(0, strlen($characters) - 1)];
        }
        return $string;
    }
}

/**
 * ExceptionControllerError
 *
 * @uses Apricot_Controller
 * @package Apricot
 * @author Jansen Price <jansen.price@gmail.com>
 * @license http://www.opensource.org/licenses/mit-license.php MIT
 * @version ApricotVersion: 1.1
 */
class ExceptionHandler_ControllerError extends Apricot_Controller
{
    /**
     * action__index
     *
     * @return void
     */
    public function action__index()
    {
        $this->vbuf('<h2>An Error Occurred</h2>');
        $error_number = Apricot_ExceptionHandler::log_exception(
            $this->_request->exception
        );

        if ($this->_kernel->getConfigSetting('env') != 'production') {
            $html = '<h3>' . $this->_request->exception->getMessage() . '</h3>'
                . "<p>Http Status Code: " . $this->_request->httpcode . "</p>"
                . "<p>Error logged as number " . $error_number . "</p>"
                . Apricot_ExceptionHandler::display_exception(
                    $this->_request->exception
                );
            $this->vbuf($html);
        } else {
            $this->vbuf(
                "<p>Please report this error to an adminstrator. "
                . " Error number: " . $error_number . "</p>"
            );
        }
    }
}
